<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件分片上传</title>
    <!-- 引入 Vue -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <!-- 引入 Element UI 的 CSS 和 JS -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <!-- 引入 Axios -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <!-- 引入 SparkMD5 库 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
    <style>
        .el-row {
            margin-bottom: 20px;
            &:last-child {
                margin-bottom: 0;
            }
        }
        .el-col {
            border-radius: 4px;
        }
        .grid-content {
            border-radius: 4px;
            min-height: 36px;
        }
    </style>
</head>

<body>
<div id="app">

    <el-row>
        <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
        <el-col :span="8"><div class="grid-content bg-purple-light">
            <input type="file" id="fileInput" />
            <el-button @click="uploadFile" >上传文件</el-button>
            <el-input v-model="partNum" placeholder="请输入分片数"></el-input>
            <div v-if="uploadStatus">
                <p>{{ uploadStatus }}</p>
                <el-progress :percentage="uploadProgress"></el-progress>
            </div>

            <!-- 分片进度条 -->
            <div v-for="(chunk, index) in chunks" :key="index">
                <span>Chunk {{ index + 1 }}</span>
                <el-progress :percentage="chunk.progress" :status="chunk.status"></el-progress>
            </div>
        </div></el-col>
        <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
    </el-row>

    <el-row>
        <el-col :span="8"></el-col>
        <el-col :span="8">

        </el-col>
        <el-col :span="8"></el-col>
    </el-row>

</div>

<script>
        new Vue({
            el: '#app',
            data: {
                uploadStatus: '',
                cnt: 0,
                uploadProgress: 0,
                chunks: [],
                partNum: 10,
                shardUploadId: '',
                file: null,
                md5: '',
            },
            methods: {
                reset() {
                    this.uploadStatus = '';
                    this.cnt = 0;
                    this.uploadProgress = 0;
                    this.chunks = [];
                    this.shardUploadId = '';
                    this.file = null;
                    this.md5 = '';
                },
                // 初始化分片信息
                async init(){
                    this.reset();
                    this.file = document.getElementById('fileInput').files[0];
                    if (!this.file) {
                        this.uploadStatus = '请选择文件';
                        return;
                    }
                    this.md5 = await this.calculateFileMD5(this.file);
                    const chunkSize = Math.ceil(this.file.size / this.partNum);
                    for (let i = 0; i < this.partNum; i++) {
                        const start = i * chunkSize;
                        const end = Math.min(start + chunkSize, this.file.size);
                        const fileChunk = this.file.slice(start, end);
                        this.chunks.push({progress: 0, status: null,fileChunk: fileChunk,idx: i })
                    }
                },
                async uploadFile() {
                    try {
                        // 1. 初始化分片上传任务
                        await this.init();
                        const initResponse = await this.initUpload(this.file.name, this.partNum, this.md5); // 假设分片数量为 3，md5 暂时为空
                        this.shardUploadId = initResponse.data.data;
                        // 2. 分片上传
                        const results=[];
                        for (let i = 0; i < this.chunks.length; i++) {
                            var chk = this.chunks[i];
                            if(chk.status==="success"){
                                continue;
                            }

                            results.push(this.uploadPart(chk));
                        }

                        // 3. 完成分片上传
                        await Promise.all(results);
                        this.uploadStatus = '所有分片上传完成';

                        await this.completeUpload(this.shardUploadId);
                        this.uploadStatus = '所有分片合并成功';

                        // 4. 获取分片任务详细信息
                        const detailResponse = await this.getUploadDetail(this.shardUploadId);
                        this.uploadStatus += `\n 任务详情: ${JSON.stringify(detailResponse.data)}`;
                    } catch (error) {
                        this.uploadStatus = `上传失败: ${error.message}`;
                    }
                },
                // 这种内部返回promise对象跟在方法名上写async一样，写法不同而已
                calculateFileMD5(file) {
                    return new Promise((resolve, reject) => {
                        const fileReader = new FileReader();
                        const spark = new SparkMD5.ArrayBuffer();
                        let offset = 0;
                        const chunkSize = 2097152; // 2MB

                        const readChunk = () => {
                            const nextChunk = file.slice(offset, offset + chunkSize);
                            if (nextChunk.size === 0) {
                                // 文件读取完毕，计算最终的 MD5 值
                                const md5 = spark.end();
                                resolve(md5);
                                return;
                            }
                            fileReader.readAsArrayBuffer(nextChunk);
                        };

                        fileReader.onload = (e) => {
                            spark.append(e.target.result);
                            offset += chunkSize;
                            readChunk();
                        };

                        fileReader.onerror = (error) => {
                            reject(error);
                        };

                        readChunk();
                    });
                },

                async initUpload(fileName, partNum, md5) {
                    const request = {
                        fileName,
                        partNum,
                        md5
                    };
                    const response = await axios.post('/shardUpload/init', request);
                    return response;
                },
                async uploadPart(chk) {
                    const formData = new FormData();
                    formData.append('shardUploadId', this.shardUploadId);
                    formData.append('partOrder', chk.idx+1);
                    formData.append('file', chk.fileChunk);
                    const response = await axios.post('/shardUpload/uploadPart', formData, {
                        headers: {
                            'Content-Type': 'multipart/form-data'
                        }
                    });
                    if(response.data.code==="1"){
                        chk.progress=100;
                        chk.status='success';
                        // 保留小数点 后保留两位
                        this.uploadProgress = Math.round(((++this.cnt ) / this.partNum) * 100);
                    }else if(response.data.code==="0"){
                        chk.status='error';
                    }
                    return response;
                },
                async completeUpload(shardUploadId) {
                    const request = {
                        shardUploadId
                    };
                    const response = await axios.post('/shardUpload/complete', request);
                    return response;
                },
                async getUploadDetail(shardUploadId) {
                    const response = await axios.get(`/shardUpload/detail?shardUploadId=${shardUploadId}`);
                    return response;
                }
            }
        });
    </script>
</body>

</html>